${EMPTY('ti-key','Sin keywords todavia','Agrega keywords para rastrear tu posicion en Google y detectar oportunidades de contenido.', ``)}
`;
}
window.addKeyword = () => { const kw = prompt('Keyword a monitorear:'); if (kw) alert('Keyword agregada: '+kw); };
async function vNetworks() {
const id = cid();
const [accD, statusD] = await Promise.allSettled([
get(`${API}/companies/${id}/accounts`),
get(`${API}/oauth/status`),
]);
const accs = accD.value?.accounts || [];
const oauthStatus = statusD.value?.platforms || {};
const NETS = [
{ id:'facebook', name:'Facebook', desc:'Paginas de Facebook' },
{ id:'instagram', name:'Instagram', desc:'Perfil Business/Creator' },
{ id:'tiktok', name:'TikTok', desc:'Cuenta TikTok Business' },
{ id:'youtube', name:'YouTube', desc:'Canal de YouTube' },
{ id:'linkedin', name:'LinkedIn', desc:'Perfil o pagina de empresa' },
{ id:'twitter', name:'X (Twitter)', desc:'Cuenta de X' },
{ id:'whatsapp', name:'WhatsApp Business', desc:'WhatsApp Cloud API' },
{ id:'google_business', name:'Google Business', desc:'Perfil de negocio en Google' },
];
const connMap = {}; accs.forEach(a => { if (!connMap[a.platform]) connMap[a.platform] = []; connMap[a.platform].push(a); });
const totalConnected = accs.length;
const activePlatforms = Object.keys(connMap).length;
return `
${pageHero('ti-plug','Redes conectadas','Conecta tus cuentas via OAuth seguro — nunca compartimos tu contrasena', ``)}
Conexion segura por OAuth
Al conectar, te redirigimos al login oficial de cada red donde autorizas el acceso. Nunca vemos tu contraseña. Instagram debe ser cuenta Business o Creator vinculada a una página de Facebook.
${ev.data.message||'Tu cuenta esta lista para publicar.'}
`);
} else {
modal('No se pudo conectar', `
${ev.data.message||'Intenta de nuevo.'}
`);
}
};
window.addEventListener('message', handler);
// Si cierran el popup sin completar
const checkClosed = setInterval(() => {
if (popup.closed) {
clearInterval(checkClosed);
window.removeEventListener('message', handler);
setTimeout(() => { if (document.getElementById('modal-root')) { closeModal(); showView('networks'); } }, 500);
}
}, 800);
} catch(e) { alert('Error: ' + e.message); }
};
window.disconnectNet = async (id, name) => {
if (!confirm(`Desconectar ${name}?`)) return;
await del(`${API}/companies/${cid()}/accounts/${id}`).catch(e => alert(e.message));
showView('networks');
};
async function vTeam() {
const id = cid();
const d = await get(`${API}/companies/${id}/team`).catch(() => ({}));
const ROLES = { company_owner:'Propietario', company_admin:'Admin', editor:'Editor', community_manager:'Community Mgr', analyst:'Analista', viewer:'Solo lectura' };
const allMembers = [d?.owner ? {...d.owner, role:'company_owner'} : null, ...(d?.members||[])].filter(Boolean);
const ROLE_COLORS = {company_owner:'blue',company_admin:'purple',editor:'green',community_manager:'amber',analyst:'gray',viewer:'gray'};
const ROLE_ICONS = {company_owner:'ti-crown',company_admin:'ti-shield',editor:'ti-pencil',community_manager:'ti-message',analyst:'ti-chart-bar',viewer:'ti-eye'};
return `
${pageHero('ti-users','Equipo','Gestiona los miembros y sus niveles de acceso', ``)}
Colaboracion en equipo
Cada rol tiene permisos distintos. Editor: crea contenido. Community Mgr: responde mensajes. Analista: solo ve reportes. Admin: todo excepto facturacion.
${EMPTY('ti-users','Sin miembros aun','Invita a tu equipo para colaborar en el contenido', '')}
`}
Permisos por rol
${[
['company_owner','Propietario','ti-crown','blue','Todo: facturacion, equipo, config'],
['company_admin','Admin','ti-shield','purple','Todo menos facturacion'],
['editor','Editor','ti-pencil','green','Crea y programa contenido'],
['community_manager','Community Mgr','ti-message','amber','Responde inbox y aprueba'],
['analyst','Analista','ti-chart-bar','gray','Solo ve metricas y reportes'],
['viewer','Lector','ti-eye','gray','Solo lectura del contenido'],
].map(([,label,ic,color,desc])=>`
${[[`${p.max_posts_per_month===-1?'Posts ilimitados':p.max_posts_per_month+' posts/mes'}`,true],[`${p.max_social_accounts===-1?'Redes ilimitadas':p.max_social_accounts+' redes'}`,true],[`${p.feature_ai_content?'IA generativa':' Sin IA generativa'}`,!!p.feature_ai_content],[`${p.max_team_members>1?p.max_team_members+' miembros':'Solo propietario'}`,p.max_team_members>1]].map(([feat,ok])=>`
${feat}
`).join('')}
${isCurrent?`
Tu plan actual
`:``}
`;
}).join('')}
window.billingPortal = async () => { try { const r = await post(`${API}/companies/${cid()}/billing/portal`,{}); if(r?.url) window.open(r.url,'_blank'); } catch(e) { alert(e.message); } };
window.checkout = async planId => { try { const r = await post(`${API}/companies/${cid()}/billing/checkout`,{planId,billingCycle:'monthly'}); if(r?.url) location.href=r.url; } catch(e) { alert(e.message); } };
async function vSettings() {
const id = cid();
const d = await get(`${API}/companies/${id}`).catch(() => ({}));
return `
${pageHero('ti-settings','Configuracion','Ajustes de tu empresa, cuenta y preferencias de la IA', ``)}
Voz de marca
La Voz de marca es la instruccion que la IA usa en cada generacion de contenido. Cuanto mas detallada, mas consistente sera tu contenido. Incluye tono, estilo, palabras que evitar y publico objetivo.
Informacion de la empresa
Voz de marca para la IA
Importante
Consejos para una buena voz de marca:
${['Define el tono (formal/informal/inspiracional/divertido)','Especifica tu publico objetivo (edad, pais, intereses)','Menciona palabras o frases que EVITAR','Lista los valores y diferenciadores de tu marca','Incluye ejemplos de posts que te gustan'].map(t=>`
${[
['Post publicado exitosamente','Cuando una publicacion se publica correctamente',true],
['Error al publicar','Si falla una publicacion programada',true],
['Nuevo comentario o mensaje','Actividad en tu Bandeja de entrada',false],
['Reporte semanal','Resumen de desempeno cada lunes',true],
['Trial por terminar','Aviso 3 dias antes de que expire el trial',true],
['Articulo de blog generado','Cuando la IA termina un articulo',false],
].map(([label,desc,checked])=>`
${label}
${desc}
`).join('')}
Zona peligrosa
Exportar mis datos
Descarga todos tus posts y configuracion
Eliminar empresa
Elimina todos los datos permanentemente
`;
}
window.saveSettings = async () => {
try {
await put(`${API}/companies/${cid()}`,{name:document.getElementById('set-name')?.value,website_url:document.getElementById('set-web')?.value,industry:document.getElementById('set-ind')?.value,brand_voice:document.getElementById('set-voice')?.value});
setAlert('set-alert','Cambios guardados correctamente','ok');
setTimeout(()=>setAlert('set-alert','','ok'),3000);
} catch(e) { setAlert('set-alert',e.message); }
};
window.changePass = () => modal('Cambiar contrasena',`
`);
window.doChangePass = async () => {
const oldP=document.getElementById('cp-old')?.value,newP=document.getElementById('cp-new')?.value,conf=document.getElementById('cp-conf')?.value;
if(!oldP||!newP){setAlert('cp-alert','Todos los campos son requeridos');return}
if(newP!==conf){setAlert('cp-alert','Las contrasenhas no coinciden');return}
if(newP.length<8){setAlert('cp-alert','Minimo 8 caracteres');return}
try{await post(`${API}/auth/change-password`,{currentPassword:oldP,newPassword:newP});closeModal();alert('Contrasena cambiada exitosamente')}
catch(e){setAlert('cp-alert',e.message)}
};
// ── COMPANIES ─────────────────────────────────────────────────
async function vCompanies() {
const id = cid();
const [dR, accsR, postsR, teamR] = await Promise.allSettled([
get(`${API}/companies/${id}`),
get(`${API}/companies/${id}/accounts`),
get(`${API}/companies/${id}/posts?limit=200`),
get(`${API}/companies/${id}/team`),
]);
const d = dR.value || {};
const nets = (accsR.value?.accounts || []).filter(a => a.is_active);
const posts = postsR.value?.posts || [];
const team = teamR.value ? [ ...(teamR.value.owner ? [teamR.value.owner] : []), ...(teamR.value.members || []) ] : [];
const now = new Date();
const postsThisMonth = posts.filter(p => { const t = new Date(p.published_at||p.created_at); return t.getMonth()===now.getMonth() && t.getFullYear()===now.getFullYear(); }).length;
const scheduled = posts.filter(p => p.status==='scheduled').length;
const published = posts.filter(p => p.status==='published').length;
const stats = [
{ label:'Redes activas', value:nets.length, ic:'ti-plug-connected', grad:'linear-gradient(135deg,#5B4FE0,#7C5CFC)' },
{ label:'Posts este mes', value:postsThisMonth, ic:'ti-send', grad:'linear-gradient(135deg,#0EA5E9,#38BDF8)' },
{ label:'Programados', value:scheduled, ic:'ti-clock', grad:'linear-gradient(135deg,#D97706,#FBBF24)' },
{ label:'Miembros equipo', value:d.team_count||team.length||1, ic:'ti-users', grad:'linear-gradient(135deg,#16A34A,#4ADE80)' },
];
// Multi-company: fetch all companies for this user
const companiesListR = await get(`${API}/companies`).catch(()=>({companies:[]}));
const allCompanies = companiesListR?.companies || [];
const currentCid = cid();
return `
${pageHero('ti-building','Empresas','Gestiona el perfil, las redes y el equipo de cada empresa', ``)}
${allCompanies.length>1?`
`).join('') : EMPTY('ti-users','Solo tu por ahora','Invita a tu equipo para colaborar', ``)}
`;
}
// ── VIDEOS CON AVATAR ─────────────────────────────────────────
async function vVideos() {
// Avatares: ranuras configurables. El usuario sube/licencia sus propias imagenes
// (con derechos para uso comercial). NO se incrustan fotos de personas reales scrapeadas.
// avatarSource lee de la biblioteca de medios de la empresa cuando exista.
const id = cid();
const lib = await get(`${API}/companies/${id}/media?type=image`).catch(()=>({files:[]}));
const avatars = (lib?.files||[]);
const VID_DURS = ['15s','30s','60s','90s'];
const VID_FMTS = [{id:'9:16',label:'9:16 Vertical',ic:'ti-device-mobile',note:'Reels & Stories'},{id:'1:1',label:'1:1 Cuadrado',ic:'ti-square',note:'Feed'},{id:'16:9',label:'16:9 Landscape',ic:'ti-device-tv',note:'YouTube'}];
const VID_MUSIC = [{id:'corporativo',label:'Corporativo',emoji:'💼'},{id:'energetico',label:'Energetico',emoji:'⚡'},{id:'suave',label:'Suave',emoji:'🎵'},{id:'ninguna',label:'Sin musica',emoji:'🔇'}];
return `
${pageHero('ti-user-circle','Avatar IA','Crea videos con tu imagen para Reels, Stories y TikTok', ``)}
1
Elige tu avatar
La imagen de fondo del video
${avatars.length ? `
${avatars.map((a,i)=>`
${i===0?`
`:''}
`).join('')}
Agregar
` : `
Sin avatares aun
Sube fotos propias, de tu marca, o generadas por IA con licencia comercial. Se muestran aqui como opciones de avatar.
Solo imagenes con derechos de uso comercial
`}
2
Texto del video
Se superpone sobre la imagen
0/300
PequeñoGrande
3
Configuracion del video
Formato, duracion y musica
${VID_FMTS.map((f,i)=>`
`).join('')}
${VID_DURS.map((d,i)=>`
`).join('')}
${VID_MUSIC.map((m,i)=>`
`).join('')}
${[{id:'es-CR',label:'🇨🇷 Español CR'},{id:'es-ES',label:'🇪🇸 Español ES'},{id:'en-US',label:'🇺🇸 English'}].map((v,i)=>`
`).join('')}
Requiere HeyGen API configurada en el backoffice para sintesis de avatar
La superposicion de texto funciona sin APIs externas. La sintesis de avatar requiere HeyGen configurado.
`;
}
// Helpers de seleccion para vVideos
window.selectVidFmt = (id, btn) => {
document.getElementById('vid-fmt').value = id;
document.querySelectorAll('#vid-fmt-grid button').forEach(b => {
const a = b.dataset.fmt === id;
b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)';
b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)';
b.querySelector('i').style.color = a ? 'var(--indigo)' : 'var(--color-text-secondary)';
});
};
window.selectVidDur = (id, btn) => {
document.getElementById('vid-dur').value = id;
document.querySelectorAll('#vid-dur-grid button').forEach(b => {
const a = b.dataset.dur === id;
b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)';
b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)';
});
};
window.selectVidMusic = (id, btn) => {
document.getElementById('vid-music').value = id;
document.querySelectorAll('#vid-music-grid button').forEach(b => {
const a = b.dataset.music === id;
b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)';
b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)';
});
};
window.selectVidVoice = (id, btn) => {
document.getElementById('vid-voice').value = id;
document.querySelectorAll('#vid-voice-grid button').forEach(b => {
const a = b.dataset.voice === id;
b.style.border = a ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)';
b.style.background = a ? 'var(--indigo-light)' : 'var(--color-background-primary)';
});
};
window.selAvatar = (el, url) => {
document.querySelectorAll('.avatar-pick').forEach(a=>a.classList.remove('selected'));
el.classList.add('selected');
window._selectedAvatar = url;
updateAvatarPreview();
};
window.updateAvatarPreview = () => {
const txt = document.getElementById('av-text')?.value || '';
const size = document.getElementById('av-fontsize')?.value || 5;
const cc = document.getElementById('av-charcount'); if (cc) cc.textContent = txt.length;
const img = document.getElementById('av-preview-img');
if (window._selectedAvatar && img) {
img.innerHTML = ``;
img.className = '';
}
const t = document.getElementById('av-preview-text');
if (t) { t.textContent = txt; t.style.fontSize = (size*2.2)+'px'; }
};
window.genVideo = () => alert('Configura una API de avatares (HeyGen) en el backoffice para generar el video. La superposicion de texto e imagen ya funciona en la vista previa.');
window.selAv = el => { document.querySelectorAll('.avatar-card').forEach(c=>c.classList.remove('selected')); el.classList.add('selected'); };
window.genVScript = async () => {
const btn = document.getElementById('vid-script-btn'); if(!btn) return;
btn.disabled = true; btn.innerHTML = ' Generando guion...';
try {
const r = await post(`${API}/companies/${cid()}/ai/generate`, { topic: 'Guion de video corto de 30 segundos presentando nuestra empresa y sus servicios, con gancho inicial y llamada a la accion', platform: 'tiktok', tone: 'informal', language: 'es', variants: 1 });
const v = r?.variants || [];
if (v.length) { const ta = document.getElementById('video-script'); if(ta) ta.value = v[0].content; }
} catch(e) { alert('Error: ' + e.message); }
finally { btn.disabled = false; btn.innerHTML = 'Generar guion con IA'; }
};
window.genVideo = () => alert('Para generar videos con avatar necesitas configurar la API key de HeyGen en el backoffice admin (admin.socialflowai.cloud).');
// ── BIBLIOTECA ────────────────────────────────────────────────
async function vLibrary() {
const id = cid();
const d = await get(`${API}/companies/${id}/media`).catch(() => ({ media: [] }));
const media = d?.media || [];
const imgs = media.filter(m=>m.media_type!=='video');
const vids = media.filter(m=>m.media_type==='video');
const totalSize = media.reduce((s,m)=>s+(m.file_size||0),0);
const fmtSize = b => b>1048576?(b/1048576).toFixed(1)+' MB':b>1024?(b/1024).toFixed(0)+' KB':b+' B';
return `
${pageHero('ti-photo','Biblioteca de medios','Almacena y gestiona tus archivos multimedia en la nube', ``)}
Almacenamiento en la nube
Sube imagenes y videos una vez y usalos en publicaciones o como avatar. Formatos: JPG, PNG, GIF, MP4 hasta 50 MB. Los archivos se guardan en tu cuenta privada.
`;}
try{
const id=cid();
for(const f of Array.from(files)){
const fd=new FormData();fd.append('file',f);
await fetch(`${API}/companies/${id}/media`,{method:'POST',headers:{Authorization:`Bearer ${gt()}`},body:fd});
}
if(prog)prog.style.display='none';
showView('library');
}catch(e){if(prog)prog.innerHTML=`
${e.message||'Error al subir'}
`;}
};
window.copyLibUrl = url => { navigator.clipboard?.writeText(url).then(()=>{const t=document.createElement('div');t.textContent='URL copiada ✓';t.style.cssText='position:fixed;bottom:24px;right:24px;background:var(--indigo);color:#fff;padding:10px 20px;border-radius:10px;font-size:13px;font-weight:600;z-index:9999';document.body.appendChild(t);setTimeout(()=>t.remove(),2000);}); };
window.delMedia = async (mid, url) => {
if(!confirm('Eliminar este archivo? Esta accion no se puede deshacer.')) return;
try{if(mid) await del(`${API}/companies/${cid()}/media/${mid}`).catch(()=>{});showView('library');}catch(e){alert(e.message);}
};
window.libFilter = (type, btn) => {
document.querySelectorAll('#lib-filter-btns button').forEach(b=>{b.style.border='1.5px solid var(--color-border-tertiary)';b.style.background='var(--color-background-primary)';});
btn.style.border='1.5px solid var(--indigo)';btn.style.background='var(--indigo-light)';
document.querySelectorAll('#lib-grid .lib-card').forEach(c=>{c.style.display=(type==='all'||c.dataset.type===type)?'':'none';});
};
window.setLibView = (mode, btn) => {
document.querySelectorAll('[onclick*=setLibView]').forEach(b=>{b.style.border='0.5px solid var(--color-border-tertiary)';b.style.background='var(--color-background-primary)';});
btn.style.border='0.5px solid var(--indigo)';btn.style.background='var(--indigo-light)';
const grid=document.getElementById('lib-grid');
if(!grid) return;
grid.style.gridTemplateColumns=mode==='list'?'1fr':'repeat(auto-fill,minmax(160px,1fr))';
};
// ── RECOMENDACIONES ───────────────────────────────────────────
async function vRecommendations() {
const id = cid();
const d = await get(`${API}/companies/${id}/recommendations`).catch(() => []);
let recos = Array.isArray(d) ? d : (d?.recommendations || []);
if (!recos.length) {
try { const gen = await post(`${API}/companies/${id}/ai/recommendations`, {}); recos = gen?.recommendations || []; } catch {}
}
const finalRecos = recos.length ? recos : [
{ title:'Timing optimo de publicacion', description:'Tus publicaciones de jueves entre 6-8pm obtienen 3x mas engagement. Mueve los posts programados a este horario.', action_label:'Aplicar automaticamente', type:'timing' },
{ title:'Formato de contenido sugerido', description:'Los Reels de 15-30s tienen 3.2x mas alcance que fotos estaticas para tu audiencia. Considera convertir 3 posts a video.', action_label:'Generar Reels', type:'format' },
{ title:'Hashtags en tendencia', description:'#IA2025 y #Marketing estan trending +180% esta semana. Usarlos en tus proximas 5 publicaciones puede aumentar el alcance.', action_label:'Actualizar hashtags', type:'hashtags' },
{ title:'Audiencia y segmentacion', description:'El 68% de tu audiencia tiene entre 25-34 anios. Ajusta el tono del contenido para captar mayor engagement en este segmento.', action_label:'Ver estrategia', type:'audience' },
{ title:'Oportunidad de crecimiento', description:'TikTok tiene el mayor engagement rate (11.4%). Incrementar de 18 a 30 posts mensuales puede triplicar el alcance.', action_label:'Ver plan', type:'growth' },
];
const iconMap = { timing:'ti-clock', format:'ti-photo', hashtags:'ti-hash', audience:'ti-target', growth:'ti-chart-line', content:'ti-bulb', general:'ti-bulb' };
const colorMap = { timing:'#2563EB', format:'#0EA5E9', hashtags:'#7C3AED', audience:'#DC2626', growth:'#16A34A', content:'#F59E0B', general:'#F59E0B' };
return `
${pageHero('ti-bulb','Recomendaciones','Sugerencias inteligentes de IA para mejorar tus resultados')}
${finalRecos.slice(0,3).map(r => `
${r.title}
${r.description}
${r.impact_estimate?`
${r.impact_estimate}
`:''}
`).join('')}
${finalRecos.slice(3,5).map(r => `
${r.title}
${r.description}
`).join('')}
`;
}
window.applyReco = async (id, label) => {
if (id) await post(`${API}/companies/${cid()}/recommendations/${id}/apply`, {}).catch(()=>{});
const l = label.toLowerCase();
if (l.includes('reel') || l.includes('video')) showView('videos');
else if (l.includes('hashtag')) showView('ai');
else if (l.includes('plan') || l.includes('estrategia')) showView('analytics');
else showView('create');
};
window.refreshRecos = async () => {
const el = document.getElementById('view-content'); el.innerHTML = SP;
try { await post(`${API}/companies/${cid()}/ai/recommendations`, {}); } catch {}
el.innerHTML = await vRecommendations();
};
// ════════════════════════════════════════════════════════════
// BLOG SEO AUTOMATICO — calendario de contenido + plan (estilo AutoSEO)
// ════════════════════════════════════════════════════════════
let blogCalRef = new Date();
async function vBlog() {
const id = cid();
const [stats, articles, cfg] = await Promise.allSettled([
get(`${API}/companies/${id}/blog/stats`),
get(`${API}/companies/${id}/blog/articles?limit=300`),
get(`${API}/companies/${id}/blog/config`),
]);
const s = stats.value || {};
const arts = articles.value?.articles || [];
const config = cfg.value || {};
const hasContent = arts.length > 0;
// Stat header (estilo AutoSEO)
const blogConnected = !!(config.webhook_verified && config.webhook_url) || config.blog_type === 'wordpress';
const blogHeaderAction = blogConnected
? `
Blog conectado
`
: ``;
const statHeader = `
${pageHero('ti-article','Blog SEO Automatico','Articulos optimizados que Google adora, publicados automaticamente en tu sitio', blogHeaderAction)}
Como funciona
La IA genera un plan en clusters tematicos, escribe cada articulo con SEO optimizado y lo publica automaticamente en tu sitio. Configuras una vez y el sistema trabaja solo.
Articulos generados
${arts.length}
Palabras escritas
${fmt(s.total_words||0)}
Ahorro vs redactor
$${fmt(s.savings_usd||0)}
Horas ahorradas
${fmt(s.time_saved_hours||0)}h
`;
if (!hasContent) {
// Estado vacío: invitar a generar el plan
return statHeader + `
Blog SEO automatico con IA
La IA genera tu calendario editorial, escribe cada articulo y lo publica solo en tu sitio.
IA genera el plan
Clusters tematicos y keywords de alto potencial para tu nicho
Escribe cada articulo
~3000 palabras con H1/H2, meta SEO, FAQ y tabla de contenidos
Publica automatico
Envia a tu sitio via webhook o WordPress sin intervencion manual
`;
}
window.blogTab = async (tab) => {
['calendar','list','analytics','config'].forEach(t => document.getElementById('bt-'+t)?.classList.toggle('active', t===tab));
const el = document.getElementById('blog-tab-content');
if (!el) return;
el.innerHTML = SP;
if (tab === 'analytics') { el.innerHTML = await renderBlogAnalytics(); return; }
const arts = (await get(`${API}/companies/${cid()}/blog/articles?limit=300`).catch(()=>({})))?.articles || [];
if (tab === 'calendar') el.innerHTML = renderBlogCalendar(arts);
else if (tab === 'list') el.innerHTML = renderBlogList(arts);
else if (tab === 'config') el.innerHTML = await renderBlogConfig();
};
async function renderBlogAnalytics() {
const d = await get(`${API}/companies/${cid()}/blog/analytics`).catch(()=>({articles:[],total_views:0,total_clicks:0,tracking_active:false}));
const arts = d.articles || [];
const snip = await get(`${API}/companies/${cid()}/blog/tracking-snippet`).catch(()=>({snippet:''}));
const notice = d.tracking_active
? `
Tracking activo: estos son datos reales reportados por tu sitio.
`
: `
Aun no hay visitas registradas. Los articulos publicados ya incluyen el pixel de tracking automaticamente; los numeros apareceran cuando alguien visite tus articulos en tu sitio. Tambien puedes pegar este snippet en tu plantilla del blog para medir todas las paginas.
`;
return `
${notice}
Visitas totales
${fmt(d.total_views||0)}
Clicks totales
${fmt(d.total_clicks||0)}
Articulos publicados
${arts.length}
Historico de publicaciones
${arts.length ? `
Articulo
Publicado
Visitas
Clicks
${arts.map(a=>`
${a.title}
${a.slug?`
/${a.slug}
`:''}
${a.published_at?fmtDT(a.published_at):'—'}
${fmt(a.views||0)}
${fmt(a.clicks||0)}
`).join('')}
` : EMPTY('ti-chart-bar','Sin articulos publicados aun','Publica articulos para empezar a medir su trafico')}
${snip.snippet ? `
Snippet de tracking (opcional)
Los articulos autopublicados ya lo incluyen. Si quieres medir TODO tu blog, pega esto antes de </body> en tu plantilla:
${snip.snippet.replace(//g,'>')}
` : ''}`;
}
function renderBlogCalendar(arts) {
const byDate = {};
arts.forEach(a => { if (a.scheduled_for) { const k = a.scheduled_for.slice(0,10); (byDate[k] = byDate[k]||[]).push(a); } });
const ref = blogCalRef;
const year = ref.getFullYear(), month = ref.getMonth();
const first = new Date(year, month, 1);
const startDay = (first.getDay()+6)%7; // lunes=0
const daysInMonth = new Date(year, month+1, 0).getDate();
const monthName = ref.toLocaleDateString('es-CR',{month:'long',year:'numeric'});
const today = new Date().toISOString().slice(0,10);
let cells = '';
for (let i=0;i`;
for (let d=1; d<=daysInMonth; d++) {
const dateStr = `${year}-${String(month+1).padStart(2,'0')}-${String(d).padStart(2,'0')}`;
const dayArts = byDate[dateStr]||[];
const isToday = dateStr===today;
cells += `
${st==='planned'?``:''}
${(st==='draft'||st==='scheduled')?` `:''}
${st==='published'?`${a.live_url?` Ver live`:bdg('Live','green')}`:''}
${st==='failed'?``:''}
`;
}).join('')}
`;
}
async function renderBlogConfig() {
const c = await get(`${API}/companies/${cid()}/blog/config`).catch(()=>({}));
const tog = (k,label,desc) => `
${label}
${desc}
`;
return `
Publicacion automatica
${tog('auto_publish','Auto-publicar articulos','Publica automaticamente en tu sitio sin revision manual')}
${tog('include_hero_image','Incluir imagen destacada','Genera una imagen IA al inicio de cada articulo')}
${tog('include_key_takeaways','Incluir puntos clave','Caja resumen con 4-5 bullets despues de la intro')}
${tog('include_toc','Tabla de contenidos','Indice clickeable con enlaces a cada seccion')}
${tog('include_faq','Preguntas frecuentes','Seccion FAQ al final de cada articulo')}
${tog('include_external_links','Enlaces externos','Agrega enlaces autoritativos (ej. Wikipedia)')}
Longitud del articulo
Frecuencia de publicacion
Autopost a tu sitio (Webhook)
SocialFlow enviara cada articulo a tu sitio mediante un POST firmado. Implementa un endpoint que reciba el JSON y publique el post.
POST { event, article:{ title, slug, html, meta_description, hero_image_url }, site }
Header: X-SocialFlow-Signature: sha256=... (HMAC con tu secret)
Call-to-Action e instrucciones
`;
}
window.openBlogConnect = async () => {
const id = cid();
const cfg = await get(`${API}/companies/${id}/blog/config`).catch(()=>({}));
const comp = await get(`${API}/companies/${id}`).catch(()=>({}));
const verified = cfg.webhook_verified;
const lastStatus = cfg.webhook_last_status;
modal('Conectar el blog de tu sitio web', `
Conecta tu sitio para que los articulos generados con IA se publiquen automaticamente en tu blog. Funciona con WordPress, Webflow, Ghost o cualquier sitio que reciba un webhook.
Tu sitio recibira un POST con el articulo (titulo, contenido HTML, imagen, meta). Te damos un secret para validar la firma.
Secret (guardalo, solo se muestra una vez): ${r.secret}
`;
} catch(e) { setAlert('wh-alert', e.message, 'err'); }
};
window.testWebhookBtn = async () => {
setAlert('wh-alert','Enviando ping de prueba...','info');
try {
const r = await post(`${API}/companies/${cid()}/blog/webhook/test`, {});
setAlert('wh-alert', r.success ? `Conexion exitosa (HTTP ${r.status})` : `Fallo: ${r.error||'HTTP '+r.status}`, r.success?'ok':'err');
} catch(e) { setAlert('wh-alert', e.message, 'err'); }
};
// ════════════════════════════════════════════════════════════
// BACKLINKS — comparacion vs competidores (estilo AutoSEO)
// ════════════════════════════════════════════════════════════
async function vBacklinks() {
const id = cid();
const d = await get(`${API}/companies/${id}/blog/competitors`).catch(()=>({competitors:[], moz_configured:false}));
const comps = d?.competitors || [];
const mozOn = !!d?.moz_configured;
const myDomains = d?.my_referring_domains; // null si no hay dato real
const myDA = d?.my_domain_authority;
const sorted = [...comps].sort((a,b)=>(b.referring_domains||0)-(a.referring_domains||0));
const withData = sorted.filter(c=>(c.referring_domains||0)>0);
const median = withData.length ? withData[Math.floor(withData.length/2)]?.referring_domains || 0 : 0;
const gap = (myDomains>0 && median>0) ? (median/myDomains).toFixed(1)+'x' : '—';
// Datos para grafico comparativo (mi sitio + competidores con dato real)
const chartRows = [
...(myDomains!=null ? [{ domain:(d.website||'Tu sitio').replace(/^https?:\/\/(www\.)?/,''), rd:myDomains, da:myDA||0, mine:true }] : []),
...withData.map(c=>({ domain:c.domain, rd:c.referring_domains||0, da:c.domain_authority||0, mine:false })),
].sort((a,b)=>b.rd-a.rd);
const maxRd = Math.max(...chartRows.map(r=>r.rd), 1);
return `
${pageHero('ti-link','Analisis de Backlinks','Compara tu autoridad de dominio con la competencia para decidir tu estrategia SEO',
``)}
Estrategia de backlinks
Mas dominios de referencia = mas autoridad para Google. Compara tu sitio con competidores e identifica la brecha. Conecta Moz API para ver datos reales en tiempo real.
${!mozOn ? `
Conecta datos reales de backlinks. Para ver dominios de referencia y autoridad reales (tuyos y de competidores), agrega tu Moz Access ID y Secret Key en el backoffice (Config -> API Keys -> Moz). Sin eso, no mostramos numeros porque no serian reales. Obtener claves de Moz
` : ''}
Tus dominios de referencia
${myDomains!=null?fmt(myDomains):'—'}
Tu autoridad de dominio
${myDA!=null?myDA:'—'}
Brecha vs mediana
${gap}
Comparativa de dominios de referencia
${chartRows.length ? `
${chartRows.map(r=>`
${r.mine?' ':''}${r.domain}${fmt(r.rd)} dominios · DA ${r.da}
`).join('')}
` : EMPTY('ti-chart-bar', mozOn?'Agrega competidores':'Sin datos reales aun', mozOn?'Agrega dominios de competidores para comparar':'Conecta Moz y agrega competidores para ver la comparativa')}
${EMPTY('ti-link','Sin competidores','Agrega dominios de competidores para analizar')}
`}
Como tomar decisiones con esto
${myDomains!=null
? `Tu sitio tiene ${fmt(myDomains)} dominios enlazando y autoridad ${myDA||0}. ${median>0?`La mediana de tus competidores es ${fmt(median)}.`:''} Google usa estos enlaces como "votos" de confianza. Prioriza pocos enlaces relevantes y de alta autoridad sobre muchos irrelevantes; el blog SEO automatico te ayuda a atraerlos de forma organica.`
: `Conecta Moz para ver tu perfil real de backlinks y el de tus competidores, y asi decidir donde enfocar tu estrategia de enlaces.`}
`;
}
(function(){
let scale=1,tx=0,ty=0,dragging=false,sx=0,sy=0;
function els(){ return [document.getElementById('topic-map-wrap'),document.getElementById('topic-map-inner')]; }
function applyT(){ const [,inner]=els(); if(inner) inner.style.transform=`scale(${scale}) translate(${tx}px,${ty}px)`; }
window.topicZoom = f => { scale=Math.max(0.3,Math.min(4,scale*f)); applyT(); };
window.topicZoomReset = () => { scale=1;tx=0;ty=0;applyT(); };
document.addEventListener('wheel', e => { const [w]=els(); if(!w||!w.contains(e.target)) return; e.preventDefault(); topicZoom(e.deltaY<0?1.12:0.9); },{passive:false});
document.addEventListener('mousedown', e => { const [w]=els(); if(!w||!w.contains(e.target)) return; dragging=true;sx=e.clientX-tx*scale;sy=e.clientY-ty*scale;w.style.cursor='grabbing'; });
document.addEventListener('mousemove', e => { if(!dragging) return; tx=(e.clientX-sx)/scale;ty=(e.clientY-sy)/scale;applyT(); });
document.addEventListener('mouseup', () => { dragging=false; const [w]=els(); if(w) w.style.cursor='grab'; });
})();
// ════════════════════════════════════════════════════════════
// APROBACIONES — bandeja de revisión + colaboración
// ════════════════════════════════════════════════════════════
async function vApprovals() {
const id = cid();
const d = await get(`${API}/companies/${id}/approvals`).catch(() => ({ posts: [] }));
const posts = d?.posts || [];
if (!posts.length) {
return `
${pageHero('ti-checklist','Aprobaciones','Revisa y aprueba el contenido de tu equipo', ``)}
${EMPTY('ti-checklist','Todo al día','No hay publicaciones pendientes de aprobación. Cuando un miembro del equipo cree contenido que requiera revisión, aparecerá aquí.')}
`;
}
const nUrgent = posts.filter(p=>{ const h=(Date.now()-new Date(p.created_at||0).getTime())/3600000; return h>24; }).length;
const authors = [...new Set(posts.map(p=>p.author_name).filter(Boolean))].length;
return `
${pageHero('ti-checklist','Aprobaciones','Revisa y aprueba el contenido de tu equipo', ``)}
Flujo de aprobacion
Aprobar publica el post segun su fecha programada. Solicitar cambios lo devuelve a borrador y avisa al autor. Puedes comentar en el hilo antes de decidir.
`;
};
window.addComment = async (postId) => {
const input = document.getElementById('cmt-input-'+postId);
const body = input?.value?.trim();
if (!body) return;
await post(`${API}/companies/${cid()}/posts/${postId}/comments`, { body, type: 'comment' }).catch(e=>alert(e.message));
toggleComments(postId); toggleComments(postId);
};
window.approvePost = async (postId) => {
await post(`${API}/companies/${cid()}/posts/${postId}/approve`, {}).catch(e=>alert(e.message));
showView('approvals');
};
window.rejectPost = async (postId) => {
const reason = prompt('Motivo del rechazo (opcional):');
if (reason === null) return;
await post(`${API}/companies/${cid()}/posts/${postId}/reject`, { reason }).catch(e=>alert(e.message));
showView('approvals');
};
window.requestChanges = async (postId) => {
const body = prompt('¿Qué cambios necesita esta publicación?');
if (!body?.trim()) return;
await post(`${API}/companies/${cid()}/posts/${postId}/comments`, { body, type: 'change_request' }).catch(e=>alert(e.message));
showView('approvals');
};
// ════════════════════════════════════════════════════════════
// BANDEJA DE ENTRADA — inbox unificado de comentarios/DMs
// ════════════════════════════════════════════════════════════
let inboxFilter = { status: '', platform: '', kind: '' };
async function vInbox() {
const id = cid();
const qs = new URLSearchParams(Object.entries(inboxFilter).filter(([,v])=>v)).toString();
const d = await get(`${API}/companies/${id}/inbox${qs?'?'+qs:''}`).catch(() => ({ items: [], unread: 0 }));
const items = d?.items || [];
const filterBtn = (label, key, val) => ``;
const nComments = items.filter(i=>i.kind==='comment').length;
const nDMs = items.filter(i=>i.kind==='dm').length;
const nPositive = items.filter(i=>i.sentiment==='positive').length;
return `
${pageHero('ti-inbox','Bandeja de entrada','Comentarios y mensajes de todas tus redes', ``)}
Bandeja unificada
Comentarios, menciones y mensajes de todas tus redes en un solo lugar. Usa Sugerir con IA para responder rapido. Los puntos de color indican el sentimiento del mensaje.
${EMPTY('ti-inbox','Bandeja vacía','Aquí aparecerán los comentarios, menciones y mensajes directos de tus redes sociales conectadas. Conecta tus redes y sincroniza para empezar a recibir interacciones en un solo lugar.')}
`);
};
window.suggestReply = async (itemId) => {
const ta = document.getElementById('reply-text');
if (ta) ta.value = 'Generando...';
try {
const r = await post(`${API}/companies/${cid()}/inbox/${itemId}/suggest`, {});
if (ta) ta.value = r.suggestion || '';
} catch(e) { if (ta) ta.value = ''; setAlert('reply-alert', e.message); }
};
window.doReply = async (itemId) => {
const text = document.getElementById('reply-text')?.value?.trim();
if (!text) { setAlert('reply-alert','Escribe una respuesta'); return; }
try {
const r = await post(`${API}/companies/${cid()}/inbox/${itemId}/reply`, { text });
closeModal();
if (r.note) alert(r.note);
showView('inbox');
} catch(e) { setAlert('reply-alert', e.message); }
};
// ════════════════════════════════════════════════════════════
// PLANTILLAS — biblioteca de contenido reutilizable
// ════════════════════════════════════════════════════════════
let tplCategory = 'all';
async function vTemplates() {
const id = cid();
const d = await get(`${API}/companies/${id}/templates${tplCategory!=='all'?'?category='+tplCategory:''}`).catch(() => ({ templates: [] }));
const templates = d?.templates || [];
const CATS = [['all','Todas'],['general','General'],['promo','Promoción'],['educativo','Educativo'],['engagement','Engagement'],['festivo','Festivo']];
return `
${pageHero('ti-template','Plantillas','Contenido reutilizable para publicar mas rapido', ``)}
Ahorra tiempo
Guarda formatos que funcionan una sola vez y usalos en segundos. Clic en Usar lleva el texto directo al editor. Organiza por categoria para encontrarlos rapido.
${EMPTY('ti-template','Sin plantillas','Crea plantillas reutilizables para no escribir el mismo contenido una y otra vez. Ideal para promociones recurrentes, saludos festivos o formatos que funcionan.', ``)}
`);
};
window.editTemplate = async (id) => {
const d = await get(`${API}/companies/${cid()}/templates`).catch(()=>({templates:[]}));
const tpl = (d.templates||[]).find(t=>t.id===id);
if (tpl) newTemplate(tpl);
};
window.saveTemplate = async (id) => {
const name = document.getElementById('tpl-name')?.value?.trim();
const body = document.getElementById('tpl-body')?.value?.trim();
if (!name || !body) { setAlert('tpl-alert','Nombre y contenido son requeridos'); return; }
const hashtags = (document.getElementById('tpl-tags')?.value||'').split(',').map(s=>s.trim().replace(/^#/,'')).filter(Boolean);
const payload = { name, body, category: document.getElementById('tpl-cat')?.value, hashtags };
try {
if (id) await put(`${API}/companies/${cid()}/templates/${id}`, payload);
else await post(`${API}/companies/${cid()}/templates`, payload);
closeModal(); showView('templates');
} catch(e) { setAlert('tpl-alert', e.message); }
};
window.useTemplate = async (id) => {
const d = await get(`${API}/companies/${cid()}/templates`).catch(()=>({templates:[]}));
const tpl = (d.templates||[]).find(t=>t.id===id);
if (!tpl) return;
await post(`${API}/companies/${cid()}/templates/${id}/use`, {}).catch(()=>{});
// Llevar el contenido a Crear publicación
sessionStorage.setItem('sf_template', JSON.stringify(tpl));
showView('create');
};
window.delTemplate = async (id) => {
if (!confirm('¿Eliminar esta plantilla?')) return;
await del(`${API}/companies/${cid()}/templates/${id}`).catch(e=>alert(e.message));
showView('templates');
};
// ════════════════════════════════════════════════════════════
// REPORTES PDF — white-label
// ════════════════════════════════════════════════════════════
async function vReports() {
const id = cid();
const apiBase = API.replace('/api/v1','');
const today = new Date().toISOString().slice(0,10);
const monthAgo = new Date(Date.now()-30*86400000).toISOString().slice(0,10);
// Periodos predefinidos
const last7 = new Date(Date.now()-7*86400000).toISOString().slice(0,10);
const last90 = new Date(Date.now()-90*86400000).toISOString().slice(0,10);
return `
${pageHero('ti-file-text','Reportes PDF','Reportes profesionales white-label para tus clientes', ``)}
Reportes white-label
Genera reportes profesionales con tu logo y colores para entregar a clientes. Se abren en una nueva pestana — usa Cmd/Ctrl+P → Guardar como PDF para descargar.
${[
['rep-sec-overview','Resumen ejecutivo','Alcance, engagement e impresiones del periodo',true],
['rep-sec-posts','Top publicaciones','Las 10 publicaciones con mejor desempeno',true],
['rep-sec-growth','Crecimiento de seguidores','Evolucion de seguidores por red',true],
['rep-sec-audience','Audiencia','Datos demograficos y geograficos',false],
['rep-sec-blog','Blog SEO','Visitas y posicionamiento de articulos',false],
['rep-sec-reco','Recomendaciones IA','Sugerencias automaticas para el proximo periodo',true],
].map(([id,label,desc,checked])=>`
${[['1','Haz clic en Generar reporte','Se abre en nueva pestana'],['2','Presiona Cmd+P (Mac) o Ctrl+P (Windows)','Abre el dialogo de impresion'],['3','Destino: Guardar como PDF','Selecciona en la lista de impresoras'],['4','Haz clic en Guardar','PDF listo para enviar al cliente']].map(([n,t,d])=>`
${n}
${t}
${d}
`).join('')}
`;
}
window.setRepPeriod = (start, end, btn) => {
if (start) { document.getElementById('rep-start').value = start; document.getElementById('rep-end').value = end; }
document.querySelectorAll('#rep-preset-grid button').forEach(b => {
b.style.border = b === btn ? '1.5px solid var(--indigo)' : '1.5px solid var(--color-border-tertiary)';
b.style.background = b === btn ? 'var(--indigo-light)' : 'var(--color-background-primary)';
});
};
window.updateRepPreview = () => {
const color = document.getElementById('rep-color')?.value || '#2563EB';
const name = document.getElementById('rep-agency')?.value || 'Tu Agencia';
const hdr = document.getElementById('rep-preview-header');
const nm = document.getElementById('rep-preview-name');
if (hdr) hdr.style.background = color;
if (nm) nm.textContent = name;
};
window.genReport = () => {
const start = document.getElementById('rep-start')?.value;
const end = document.getElementById('rep-end')?.value;
const tk = localStorage.getItem('sf_token') || sessionStorage.getItem('sf_token') || '';
const url = `${API}/companies/${cid()}/report?start=${start}&end=${end}`;
// Abrir con auth via fetch y blob (porque requiere token)
fetch(url, { headers: { Authorization: 'Bearer ' + tk } })
.then(r => r.text())
.then(html => {
const w = window.open('', '_blank');
w.document.write(html); w.document.close();
})
.catch(e => alert('Error generando reporte: ' + e.message));
};
window.saveReportBranding = async () => {
const payload = {
report_agency_name: document.getElementById('rep-agency')?.value,
report_logo_url: document.getElementById('rep-logo')?.value,
report_accent: document.getElementById('rep-color')?.value,
};
try { await put(`${API}/companies/${cid()}`, payload); alert('Marca guardada'); }
catch(e) { alert(e.message); }
};
// ════════════════════════════════════════════════════════════
// AUDIENCIA — mapa por region + heatmap de actividad
// ════════════════════════════════════════════════════════════
let audPlatform = 'all';
async function vAudience() {
const id = cid();
const [mapD, heatD] = await Promise.allSettled([
get(`${API}/companies/${id}/analytics/audience-map${audPlatform!=='all'?'?platform='+audPlatform:''}`),
get(`${API}/companies/${id}/analytics/activity-heatmap`),
]);
const map = mapD.value || { regions:[], has_data:false };
const heat = heatD.value || { grid:[], has_data:false };
return `
${pageHero('ti-map-2','Audiencia','Conoce de donde es tu audiencia y cuando esta mas activa', ``)}
Datos reales de tu audiencia
El mapa muestra de donde son tus seguidores por pais. El heatmap revela los dias y horas con mas engagement — usa esos datos para programar tus publicaciones.
${renderAudienceMap(map)}
${renderActivityHeatmap(heat)}`;
}
function renderAudienceMap(map) {
const PLATS = [['all','Todas'],['facebook','Facebook'],['instagram','Instagram'],['tiktok','TikTok'],['youtube','YouTube']];
if (!map.has_data) {
// Empty state como invitacion a actuar (segun guia de diseno)
return `
Audiencia por region
Aun no hay datos de audiencia
Cuando conectes tus redes y autorices el acceso, mostraremos aqui un mapa con la ubicacion de tus seguidores. Empieza conectando una cuenta.
Programa contenido en el huso horario de tu pais principal para mayor alcance.
`;
}
window.setAudPlat = (p) => { audPlatform = p; loadView(); };
function renderActivityHeatmap(heat) {
if (!heat.has_data) {
return `
Mejores horarios para publicar
Sin actividad todavia
En cuanto publiques y tus posts reciban interacciones, este mapa de calor te mostrara los dias y horas en que tu audiencia responde mejor.
`;
}
const dias = ['Lun','Mar','Mie','Jue','Vie','Sab','Dom'];
const dowOrder = [1,2,3,4,5,6,0]; // reordenar para empezar en lunes
const max = heat.max_engagement || 1;
const cellColor = (v) => {
if (v === 0) return 'var(--bg2)';
const t = v / max;
if (t > 0.75) return '#1E40AF';
if (t > 0.5) return '#2563EB';
if (t > 0.25) return '#60A5FA';
return '#BFDBFE';
};
// horas agrupadas de 3 en 3 para compactar (0-2,3-5,...21-23)
const hourBuckets = [[0,'0-3'],[3,'3-6'],[6,'6-9'],[9,'9-12'],[12,'12-15'],[15,'15-18'],[18,'18-21'],[21,'21-24']];
const bestTxt = heat.best_slot ? `${['Dom','Lun','Mar','Mie','Jue','Vie','Sab'][heat.best_slot.dow]} a las ${heat.best_slot.hour}:00` : '—';
return `
Mejores horarios para publicar
Mejor: ${bestTxt}
Basado en ${heat.total_posts} publicaciones y el engagement que recibieron. Mas oscuro = mas interaccion.
${hourBuckets.map(([,l])=>`
${l}
`).join('')}
${dowOrder.map((dow,i)=>`
${dias[i]}
${hourBuckets.map(([h])=>{
// sumar las 3 horas del bucket
let v=0; for(let k=0;k<3;k++){ v += (heat.grid[dow]&&heat.grid[dow][h+k])||0; }
return `
`;
}).join('')}
`).join('')}
Menos
${['var(--bg2)','#BFDBFE','#60A5FA','#2563EB','#1E40AF'].map(c=>``).join('')}
Mas
`;
}
// ════════════════════════════════════════════════════════════
// AUTOMATIZACION — reglas, cola evergreen, registro
// ════════════════════════════════════════════════════════════
const AUTO_TYPES = {
recycle: { label:'Reciclar contenido top', ic:'ti-recycle', desc:'Vuelve a publicar tus posts con mejor rendimiento despues de un tiempo.' },
evergreen: { label:'Cola evergreen', ic:'ti-infinity', desc:'Programa automaticamente contenido reutilizable en los huecos de tu calendario.' },
recurring: { label:'Publicacion recurrente', ic:'ti-repeat', desc:'Publica un mensaje fijo cada cierto intervalo.' },
auto_reply:{ label:'Respuesta automatica', ic:'ti-message-bolt', desc:'Responde comentarios y mensajes que contengan ciertas palabras clave.' },
};
async function vAutomation() {
const id = cid();
const [rulesD, everD, logD] = await Promise.allSettled([
get(`${API}/companies/${id}/automation/rules`),
get(`${API}/companies/${id}/automation/evergreen`),
get(`${API}/companies/${id}/automation/log`),
]);
const rules = rulesD.value?.rules || [];
const evergreen = everD.value?.items || [];
const log = logD.value?.log || [];
const activeCount = rules.filter(r=>r.enabled).length;
return `
${pageHero('ti-robot','Automatizacion','Reglas que trabajan solas mientras tu te enfocas en lo importante', ``)}
Automatiza tu presencia
Reciclar: republica tu mejor contenido. Evergreen: mantiene la cola activa. Recurrente: publica mensajes fijos. Auto-respuesta: contesta comentarios por palabra clave.
${meta.label}${r.last_run_at?' · Ultima vez: '+fmtD(r.last_run_at):' · Nunca ejecutada'}
`;
}).join('')}
` : EMPTY('ti-robot','Sin reglas activas','Crea tu primera automatizacion para ahorrar trabajo manual: recicla tu mejor contenido, manten una cola evergreen siempre activa, o responde comentarios al instante.', ``)}
`);
};
window.saveBioLink = async () => {
const label = document.getElementById('bl-label')?.value?.trim();
const url = document.getElementById('bl-url')?.value?.trim();
if (!label || !url) { setAlert('biolink-alert','Etiqueta y URL son requeridas'); return; }
try { await post(`${API}/companies/${cid()}/bio/links`, { label, url }); closeModal(); showView('biolink'); }
catch(e){ setAlert('biolink-alert', e.message); }
};
window.delBioLink = async (id) => {
if (!confirm('¿Eliminar este enlace?')) return;
try { await del(`${API}/companies/${cid()}/bio/links/${id}`); showView('biolink'); }
catch(e){ alert(e.message); }
};
// ── LOGIN ─────────────────────────────────────────────────────
// ── GUIAS CONTEXTUALES por vista ─────────────────────────────
const GUIDES = {
dashboard: { t:'Dashboard', steps:[
'Aqui ves el resumen de tus redes: alcance, engagement, impresiones y seguidores de los ultimos 30 dias.',
'Usa los accesos rapidos para ir directo a crear contenido, programar o ver metricas.',
'Los paneles de abajo muestran tus publicaciones recientes y el estado de tus redes conectadas.'] },
create: { t:'Crear publicacion', steps:[
'Escribe tu contenido y selecciona en que redes publicar (solo apareceran las que tengas conectadas).',
'Usa el boton IA junto a hashtags para que la IA sugiera etiquetas relevantes.',
'Programa con "Mejor hora" para publicar cuando tu audiencia esta mas activa.',
'El "primer comentario" se publica automaticamente al salir el post: ideal para enlaces o hashtags extra.',
'Para muchos posts a la vez, usa "Programacion masiva".'] },
calendar: { t:'Calendario', steps:[
'Ves tus publicaciones de redes y articulos del blog en un solo calendario unificado.',
'Filtra por Todo, Redes o Blog con los chips de arriba a la derecha.',
'Doble clic en un dia para crear contenido con esa fecha ya precargada.',
'El color de cada evento indica su estado: verde=publicado, azul=programado, amarillo=borrador.',
'Clic en un evento para abrirlo y editarlo directamente.'] },
automation: { t:'Automatizacion', steps:[
'Crea reglas que trabajan solas para reducir tu trabajo manual.',
'Reciclar: re-publica tu mejor contenido pasado un tiempo. Evergreen: programa contenido reutilizable en huecos.',
'Recurrente: publica un mensaje fijo cada cierto intervalo. Respuesta automatica: contesta comentarios por palabra clave.',
'Activa o pausa cada regla con el switch. El registro muestra lo que el motor ha hecho.'] },
approvals: { t:'Aprobaciones', steps:[
'Aqui llegan las publicaciones que tu equipo crea y requieren tu revision.',
'Aprueba, solicita cambios o rechaza. Puedes comentar en el hilo de cada post.',
'Solicitar cambios devuelve el post a borrador y avisa al autor.'] },
inbox: { t:'Bandeja de entrada', steps:[
'Comentarios, menciones y mensajes de tus redes en un solo lugar.',
'Filtra por sin leer, respondidos o por tipo. Responde directo o usa "Sugerir con IA".',
'Nota: requiere que tus redes esten conectadas via OAuth para recibir interacciones.'] },
templates: { t:'Plantillas', steps:[
'Guarda contenido reutilizable para no escribir lo mismo una y otra vez.',
'Organiza por categoria. Clic en "Usar" lleva el texto directo a Crear publicacion.'] },
reports: { t:'Reportes PDF', steps:[
'Selecciona el periodo: 7, 30 o 90 dias, o personaliza las fechas.',
'Configura tu marca (logo, color, nombre de agencia) para reportes white-label.',
'Elige que secciones incluir: resumen, top posts, crecimiento, audiencia, blog, recomendaciones IA.',
'Haz clic en Generar reporte — se abre en nueva pestana.',
'Para descargar como PDF: Cmd+P (Mac) o Ctrl+P (Windows) → Destino: Guardar como PDF.'] },
audience: { t:'Audiencia', steps:[
'El mapa muestra de donde son tus seguidores (se llena cuando conectas tus redes).',
'El heatmap usa tus datos reales para mostrar los mejores dias y horas para publicar.'] },
biolink: { t:'Link in Bio', steps:[
'Crea una micro-pagina con todos tus enlaces, ideal para la bio de Instagram/TikTok.',
'Personaliza titulo, descripcion, avatar, tema y color. Agrega los enlaces que quieras.',
'Comparte tu URL publica; cuenta visitas y clics por enlace. La vista previa es en vivo.'] },
blog: { t:'Blog SEO Automatico', steps:[
'La IA genera un calendario de articulos en clusters tematicos optimizados para Google.',
'Cada articulo incluye H1/H2, meta descripcion, tabla de contenidos, FAQ y enlaces externos.',
'Conecta tu sitio con webhook o WordPress para publicacion automatica sin intervencion.',
'En la tab Configuracion define frecuencia, longitud y CTA de cada articulo.',
'La tab Analitica muestra visitas y clicks reales cuando el tracking pixel esta activo.'] },
networks: { t:'Redes conectadas', steps:[
'Conecta cada red con un clic via OAuth seguro: nunca compartes tu contrasena.',
'Necesitas cuentas Business/Creator en Instagram, vinculadas a una pagina de Facebook.',
'Una vez conectadas, podras publicar, ver audiencia y recibir interacciones.'] },
analytics: { t:'Metricas', steps:[
'Tu desempeno en redes: alcance, engagement e impresiones, con cambios vs el periodo anterior.',
'Las metricas se llenan a medida que publicas y tus redes conectadas reportan datos.'] },
videos: { t:'Avatar IA', steps:[
'Sube una imagen propia o licenciada como avatar — nunca usamos fotos de terceros sin permisos.',
'Escribe el texto que quieres superponer y ajusta el tamano de fuente con el slider.',
'Elige el avatar de la galeria y genera el video. Se guarda en tu Biblioteca para reutilizarlo.'] },
library: { t:'Biblioteca de medios', steps:[
'Sube imagenes y videos una vez y usalos en cualquier publicacion futura.',
'Formatos soportados: JPG, PNG, GIF, MP4 hasta 50 MB por archivo.',
'Los archivos se guardan en la nube y estan disponibles desde el editor al crear contenido.'] },
backlinks: { t:'Analisis de Backlinks', steps:[
'Conecta tu Moz API Key en el backoffice (Config -> API Keys) para ver datos reales.',
'Agrega dominios de competidores con el boton "+ Agregar competidor" para comparar.',
'La brecha indica cuantas veces mas dominios de referencia tiene la mediana vs tu sitio.',
'Mas dominios de referencia = mas autoridad para Google = mejor posicion organica.'] },
seo: { t:'SEO & Google Ads', steps:[
'La puntuacion SEO audita los elementos tecnicos clave de tu sitio web.',
'Agrega keywords para monitorear tu posicion en Google y detectar oportunidades de contenido.',
'Rojo = falta, Amarillo = mejorar, Verde = correcto. Atiende primero los rojos.'] },
ai: { t:'IA Generativa', steps:[
'Describe el tema: cuanto mas detallado, mejor el resultado.',
'Elige la plataforma correcta: cada red tiene limites y estilos distintos.',
'Genera 3 o 5 variantes y elige la que mejor suene. Luego editala en el editor.',
'Clic en "Usar en editor" lleva el texto directo a Crear publicacion con un clic.'] },
automation: { t:'Automatizacion', steps:[
'Crea reglas con el boton "Nueva regla". Cada tipo tiene un comportamiento distinto.',
'Reciclar: republica tus posts con mas engagement pasado el tiempo configurado.',
'Evergreen: mantiene la cola activa programando contenido reutilizable en huecos.',
'Recurrente: publica un mensaje fijo (ej: promo del viernes) en el intervalo que definas.',
'Auto-respuesta: contesta comentarios que contengan palabras clave especificas.',
'Activa/pausa cada regla con el switch. El registro muestra lo que se ejecuto.'] },
networks: { t:'Redes conectadas', steps:[
'Conecta cada red con un clic — te redirigimos al login oficial, nunca vemos tu contrasena.',
'Instagram requiere cuenta Business o Creator vinculada a una pagina de Facebook.',
'Puedes conectar multiples cuentas de la misma plataforma (ej: 2 Instagram).',
'Si el boton aparece desactivado, el administrador debe configurar esa plataforma primero.',
'Revoca el acceso en cualquier momento desde la propia red o presionando el boton Desconectar.'] },
team: { t:'Equipo', steps:[
'Invita miembros por email. Recibiran un enlace para crear su cuenta y acceder.',
'Propietario: acceso total. Admin: todo excepto facturacion. Editor: crea y programa.',
'Community Mgr: responde inbox y puede aprobar. Analista: solo ve reportes.',
'Puedes cambiar el rol de un miembro en cualquier momento desde su perfil.',
'Las publicaciones de Editores pueden requerir aprobacion segun tu configuracion.'] },
billing: { t:'Facturacion', steps:[
'El plan actual se muestra con su uso del mes en barras de progreso.',
'Si llegas al 90% del limite, veras una alerta — considera actualizar antes de que falle.',
'Cambia de plan con un clic. Los cambios aplican al siguiente ciclo de facturacion.',
'Para cancelar, usa el boton Gestionar suscripcion que te lleva al portal de Stripe.',
'Los pagos son procesados exclusivamente por Stripe — nunca almacenamos datos de tarjeta.'] },
settings: { t:'Configuracion', steps:[
'La Voz de marca es la instruccion que la IA usa en cada generacion — completala bien.',
'Incluye tono, publico objetivo, palabras a evitar y valores de tu marca.',
'Las notificaciones por email se pueden personalizar individualmente.',
'Exporta tus datos en cualquier momento desde la Zona peligrosa.',
'Los cambios se guardan con el boton Guardar en la esquina superior derecha.'] },
chatbot: { t:'Chatbot IA 24/7', steps:[
'Crea un chatbot por canal (WhatsApp, Instagram, Facebook, TikTok, Web).',
'Define la personalidad: dale un nombre, un rol y una meta clara (agendar / vender / informar).',
'Conecta el canal en Redes conectadas primero para que el chatbot pueda enviar mensajes.',
'El chatbot responde en segundos, califica leads y los pasa al CRM automaticamente.',
'Revisa las conversaciones recientes para mejorar el guion de la IA.'] },
callcenter: { t:'Call Center IA', steps:[
'Crea una campana saliente: sube contactos y define el guion de la llamada.',
'La IA llama, detecta si es persona real o contestador y adapta la conversacion.',
'Configura llamadas entrantes para que la IA atienda a quienes te llaman 24/7.',
'Cada llamada queda grabada y transcrita para que puedas revisar el resultado.',
'Los leads calificados por llamada pasan automaticamente al CRM.'] },
appointments: { t:'Agendamientos', steps:[
'Conecta tu Google Calendar u Outlook para que la IA vea tu disponibilidad en tiempo real.',
'El chatbot o call center propone horarios y el cliente confirma directamente en la conversacion.',
'Activa recordatorios automaticos 24h y 1h antes para reducir las cancelaciones.',
'El seguimiento post-cita pide referidos y feedback automaticamente.',
'Configura la duracion de las citas y el buffer entre ellas en la seccion de disponibilidad.'] },
leads: { t:'Calificacion de Leads', steps:[
'Define 3-5 preguntas clave de calificacion (presupuesto, urgencia, decision).',
'El sistema asigna puntos segun las respuestas y calcula un score del 0 al 100.',
'Leads calientes (80+) son enviados al equipo de ventas de inmediato.',
'Leads tibios (50-79) entran a secuencia de nurturing automatica.',
'Leads frios (-50) se nutren con contenido hasta que esten listos.'] },
crm: { t:'CRM de Clientes', steps:[
'El CRM se llena automaticamente cuando el chatbot o call center captura un lead.',
'El pipeline visual muestra donde esta cada contacto en el proceso de venta.',
'Puedes importar contactos desde un CSV con nombre, telefono y email.',
'Cada contacto tiene historial de conversaciones, citas y puntuacion de lead.',
'Usa el boton Contactar para iniciar una conversacion desde el CRM.'] },
};
window.showGuide = () => {
const g = GUIDES[currentView];
if (!g) { modal('Guia', '
Esta seccion no tiene guia todavia. Explora los botones; cada accion tiene una etiqueta clara.
'); return; }
modal(`Como usar: ${g.t}`, `
${g.steps.map((s,i)=>`
${i+1}
${s}
`).join('')}
`, '560px');
};
// ════════════════════════════════════════════════════════════
// IA COMERCIAL — Funcionalidades inspiradas en Zolutium
// Chatbot IA 24/7, Call Center IA, Agendamientos,
// Calificación de Leads, CRM de Clientes
// ════════════════════════════════════════════════════════════
// ── CHATBOT IA 24/7 ──────────────────────────────────────────
async function vChatbot() {
const id = cid();
const [botsR, statsR, convR] = await Promise.allSettled([
get(`${API}/companies/${id}/chatbot/bots`).catch(()=>({bots:[]})),
get(`${API}/companies/${id}/chatbot/stats`).catch(()=>({})),
get(`${API}/companies/${id}/chatbot/conversations?limit=10`).catch(()=>({conversations:[]})),
]);
const bots = botsR.value?.bots || [];
const stats = statsR.value || {};
const convs = convR.value?.conversations || [];
const activeBots = bots.filter(b=>b.active).length;
return `
${pageHero('ti-message-chatbot','Chatbot IA 24/7','Atiende, califica y vende automaticamente en WhatsApp, IG, FB y TikTok', ``)}
Atencion automatica 24/7
Tu chatbot IA responde en segundos, califica leads, agenda citas y cierra ventas mientras duermes. Funciona en WhatsApp, Instagram, Facebook, TikTok y tu sitio web.
${c.is_lead?` Lead`:''}
${sentiment.label}
${c.appointment_booked?` Cita agendada`:''}
`;
}).join('')}
` : `
No hay conversaciones todavia. Activa un chatbot para empezar.
`}
Chatbot en ${fmt(stats.total_conversations||0)} conversaciones
Respondiendo 24 horas al dia
Canales disponibles
${[
{ic:'ti-brand-whatsapp',name:'WhatsApp',color:'#25D366',note:'API oficial · Mayor conversion'},
{ic:'ti-brand-instagram',name:'Instagram',color:'#E1306C',note:'DMs y comentarios'},
{ic:'ti-brand-facebook',name:'Facebook',color:'#1877F2',note:'Messenger y comentarios'},
{ic:'ti-brand-tiktok',name:'TikTok',color:'#010101',note:'Comentarios en videos'},
{ic:'ti-world',name:'Web Chat',color:'#5B4FE0',note:'Widget en tu sitio web'},
].map(ch=>`
${ch.name}
${ch.note}
`).join('')}
Mejores practicas
${['Responde en menos de 5 segundos — la velocidad multiplica las conversiones','Personaliza el saludo con el nombre del contacto','Califica con 2-3 preguntas clave antes de pasar a ventas','Programa recordatorios automaticos para quien no respondio'].map(t=>`
No hay llamadas todavia. Configura una campana para comenzar.
`}
Como funciona
${[['1','Sube contactos','Importa una lista CSV o usa leads del CRM'],['2','Define el guion','La IA aprende tu propuesta de valor y objeciones'],['3','Lanza la campana','La IA llama automaticamente y agenda citas'],['4','Revisa resultados','Ve grabaciones, transcripciones y conversiones']].map(([n,t,d])=>`
${n}
${t}
${d}
`).join('')}
Resultados tipicos
${[['85%','Reduccion de curiosos / leads no calificados'],['3x','Mas citas agendadas vs llamadas manuales'],['24/7','Atencion sin contratar mas personal'],['5 min','Tiempo de respuesta promedio de la IA']].map(([v,l])=>`
El chatbot agenda citas directamente en tu calendario, envia recordatorios automaticos y reduce las cancelaciones. Conecta con Google Calendar o Calendly.
Conecta tu calendario y el chatbot empezara a agendar citas automaticamente.
`}
Disponibilidad y recordatorios
${[['apt-reminder-24','Recordatorio 24h antes','Envia WhatsApp/SMS 24 horas antes de la cita',true],['apt-reminder-1','Recordatorio 1h antes','Envia WhatsApp/SMS 1 hora antes de la cita',true],['apt-followup','Seguimiento post-cita','Mensaje automatico 2h despues para pedir referidos',false]].map(([id,label,desc,checked])=>`
${label}
${desc}
`).join('')}
Integraciones de calendario
${[{ic:'ti-brand-google',name:'Google Calendar',color:'#4285F4',note:'Sincronizacion en tiempo real'},{ic:'ti-calendar-time',name:'Calendly',color:'#006BFF',note:'Comparte tu link de reservas'},{ic:'ti-calendar',name:'Outlook',color:'#0078D4',note:'Microsoft 365 sync'}].map(cal=>`
${cal.name}
${cal.note}
`).join('')}
El chatbot puede
${['Ver tu disponibilidad en tiempo real','Proponer horarios disponibles al cliente','Confirmar la cita y enviar recordatorios','Reagendar si el cliente lo solicita','Avisar si el cliente cancela'].map(t=>`
${t}
`).join('')}
`;
}
// ── CALIFICACION DE LEADS ─────────────────────────────────────
async function vLeads() {
const id = cid();
const [leadsR, statsR, rulesR] = await Promise.allSettled([
get(`${API}/companies/${id}/leads?limit=20`).catch(()=>({leads:[]})),
get(`${API}/companies/${id}/leads/stats`).catch(()=>({})),
get(`${API}/companies/${id}/leads/qualification-rules`).catch(()=>({rules:[]})),
]);
const leads = leadsR.value?.leads || [];
const stats = leadsR.value?.stats || statsR.value || {};
const rules = rulesR.value?.rules || [];
const hotLeads = leads.filter(l=>l.score>=80).length;
const warmLeads = leads.filter(l=>l.score>=50&&l.score<80).length;
const coldLeads = leads.filter(l=>l.score<50).length;
return `
${pageHero('ti-filter','Calificacion de Leads','Elimina curiosos y enfocate solo en quienes tienen intencion real de compra', ``)}
Filtra el +85% de curiosos
El sistema califica automaticamente cada lead con un score del 0-100 basado en sus respuestas, comportamiento y datos. Solo te llegan los que tienen intencion real.
Configura las preguntas de calificacion y el chatbot empezara a filtrar leads automaticamente.
`}
Reglas de calificacion
${rules.length ? `
${rules.map(r=>`
${r.question}
+${r.score_if_yes||0} pts si responde Si · Tipo: ${r.type||'texto'}
`).join('')}
` : `
Ejemplos de preguntas de calificacion:
${['¿Tienes un presupuesto definido para este mes?','¿Cuantas personas trabajan en tu empresa?','¿Ya usas algun CRM actualmente?','¿Necesitas la solucion en menos de 30 dias?'].map((q,i)=>`
${q}
`).join('')}
`}
Sistema de scoring
${[{label:'Caliente',range:'80-100',color:'#DC2626',bg:'#FEF2F2',desc:'Accion inmediata — llama ahora'},{label:'Tibio',range:'50-79',color:'#D97706',bg:'#FFFBEB',desc:'Seguimiento en 24h'},{label:'Frio',range:'0-49',color:'#64748B',bg:'#F8FAFC',desc:'Nurturing automatico'}].map(s=>`
${s.label}
${s.range} puntos
${s.desc}
`).join('')}
El score aumenta con cada respuesta positiva a tus preguntas de calificacion y disminuye con senales de baja intencion.
Resultado tipico
85% menos curiosos
Solo interactuas con leads que tienen presupuesto, necesidad y urgencia definidos. Tu tiempo vale mas.
`;
}
// ── CRM DE CLIENTES ──────────────────────────────────────────
async function vCRM() {
const id = cid();
const [contactsR, statsR, pipelineR] = await Promise.allSettled([
get(`${API}/companies/${id}/crm/contacts?limit=25`).catch(()=>({contacts:[]})),
get(`${API}/companies/${id}/crm/stats`).catch(()=>({})),
get(`${API}/companies/${id}/crm/pipeline`).catch(()=>({stages:[]})),
]);
const contacts = contactsR.value?.contacts || [];
const stats = statsR.value || {};
const stages = pipelineR.value?.stages || [
{id:'new',name:'Nuevo',color:'#64748B',count:0},
{id:'contacted',name:'Contactado',color:'#2563EB',count:0},
{id:'qualified',name:'Calificado',color:'#D97706',count:0},
{id:'proposal',name:'Propuesta',color:'#7C3AED',count:0},
{id:'won',name:'Ganado',color:'#16A34A',count:0},
{id:'lost',name:'Perdido',color:'#DC2626',count:0},
];
// Count by stage
contacts.forEach(c => { const s=stages.find(st=>st.id===c.stage); if(s) s.count=(s.count||0)+1; });
return `
${pageHero('ti-address-book','CRM de Clientes','Todos tus contactos, conversaciones y pipeline de ventas en un solo lugar', ``)}
CRM conectado con tu IA
Todos tus contactos, historial de conversaciones, pipeline de ventas y seguimientos automaticos en un solo lugar. Conectado con el chatbot y el call center.